/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package net.stuffrepos.bricknetcenter.util.gui.inputtable;

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import javax.swing.table.TableCellEditor;
import net.stuffrepos.bricknetcenter.util.Inflector;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import net.stuffrepos.bricknetcenter.util.validation.Constraint;
import net.stuffrepos.bricknetcenter.util.validation.ConstraintFactory;
import net.stuffrepos.bricknetcenter.util.validation.Property;
import net.stuffrepos.util.cache.CacheableValue;
import net.stuffrepos.util.listeners.Listener;
import net.stuffrepos.util.listeners.ListenerManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 *
 * @author Eduardo H. Bogoni <eduardobogoni@gmail.com>
 */
public class ObjectsTable<ObjectType> extends JTable {

    private static final Log log = LogFactory.getLog(ObjectsTable.class);
    private final List<Column> columns;
    private List<InternalObject<ObjectType>> objects = new LinkedList<InternalObject<ObjectType>>();
    private Map<ObjectType, InternalObject<ObjectType>> objectsMap = new HashMap<ObjectType, InternalObject<ObjectType>>();
    private ListenerManager<ObjectsTable<ObjectType>> valueChangeListenerManager = new ListenerManager<ObjectsTable<ObjectType>>(this);
    private CacheableValue<Boolean> objectsValid = new CacheableValue<Boolean>() {

        @Override
        protected Boolean calculate() {
            for (InternalObject<ObjectType> object : objects) {
                if (!object.isValid()) {
                    return false;
                }
            }

            return true;
        }
    };

    public ObjectsTable(Class entityClass, String[] properties) {
        this.columns = new ArrayList<Column>(properties.length);
        for (int i = 0; i < properties.length; i++) {
            this.columns.add(new Column(Property.getProperty(entityClass, properties[i])));
        }
        setModel(new Model());
        setCellSelectionEnabled(true);
    }

    @Override
    public TableCellEditor getCellEditor(int row, int column) {
        return columns.get(column).getCellEditor();
    }

    @Override
    public TableCellRenderer getCellRenderer(int row, int column) {
        if (columns.get(column).isValidValue(objects.get(row).getProperty(columns.get(column).getProperty().getPropertyName()))) {
            return columns.get(column).getValidValueCellRenderer();
        } else {
            return columns.get(column).getInvalidValueCellRenderer();
        }

    }

    public void addObject(ObjectType object) {
        objectsMap.put(object, new InternalObject<ObjectType>(object));
        objects.add(objectsMap.get(object));
        getObjectsModel().fireTableDataChanged();
    }

    public void clearObjects() {
        objects.clear();
        getObjectsModel().fireTableDataChanged();
    }

    public Model getObjectsModel() {
        return (Model) super.getModel();
    }

    public void removeObject(ObjectType object) {
        objects.remove(objectsMap.get(object));
        objectsMap.remove(object);
        getObjectsModel().fireTableDataChanged();
    }

    private void removeObject(int rowIndex) {
        InternalObject<ObjectType> internalObject = objects.get(rowIndex);
        objectsMap.remove(internalObject.getSource());
        objects.remove(rowIndex);
        getObjectsModel().fireTableDataChanged();
    }

    public void removeSelectedObjects() {
        if (!getSelectionModel().isSelectionEmpty()) {
            removeObject(getSelectionModel().getLeadSelectionIndex());
        }
    }

    public boolean isObjectsValid() {
        return objectsValid.getValue();
    }

    public Collection<InternalObject<ObjectType>> getObjects() {
        return objects;
    }

    public Collection<ObjectType> getData() {
        List<ObjectType> data = new LinkedList<ObjectType>();

        for (InternalObject<ObjectType> obj : objects) {
            data.add(obj.getData());
        }

        return data;
    }

    private class Column {

        private CacheableValue<InputCell> inputCell = new CacheableValue<InputCell>() {

            @Override
            protected InputCell calculate() {
                return InputCellFactory.newInput(property);
            }
        };
        private final Property property;

        public Column(Property property) {
            this.property = property;
        }

        private Object getObjectValue(InternalObject object) {
            return object.getProperty(property.getPropertyName());
        }

        private String getName() {
            return Inflector.humanize(property.getPropertyName());
        }

        public Class getColumnClass() {
            return property.getPropertyType();
        }

        private void setObjectValue(InternalObject object, Object value) {
            object.setProperty(property.getPropertyName(), value);
        }

        private TableCellEditor getCellEditor() {
            return inputCell.getValue().getEditor();
        }

        private TableCellRenderer getValidValueCellRenderer() {
            return inputCell.getValue().getValidValueRenderer();
        }

        public Property getProperty() {
            return property;
        }

        public boolean isValidValue(Object value) {
            if (inputCell.getValue().isValidValue(value)) {
                return getConstraint().isValid(inputCell.getValue().parseValue(value));
            } else {
                return false;
            }
        }

        public Constraint getConstraint() {

            return ConstraintFactory.getFromEntityProperty(property);
        }

        private TableCellRenderer getInvalidValueCellRenderer() {
            return inputCell.getValue().getInvalidValueRenderer();
        }

        private InputCell getInputCell() {
            return inputCell.getValue();
        }
    }

    public class Model extends AbstractTableModel {

        @Override
        public int getRowCount() {
            return objects.size();
        }

        @Override
        public int getColumnCount() {
            return columns.size();
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            return columns.get(columnIndex).getObjectValue(objects.get(rowIndex));
        }

        @Override
        public String getColumnName(int columnIndex) {
            return columns.get(columnIndex).getName();
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            return columns.get(columnIndex).getColumnClass();
        }

        @Override
        public void setValueAt(Object value, int rowIndex, int columnIndex) {
            columns.get(columnIndex).setObjectValue(objects.get(rowIndex), value);
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            return true;
        }
    }

    public class InternalObject<ObjectType> {

        private ObjectType source;
        private Map<String, Object> properties = new TreeMap<String, Object>();

        public InternalObject(ObjectType source) {
            this.source = source;
            for (Column column : columns) {
                properties.put(column.getProperty().getPropertyName(), column.getProperty().getObjectValue(source));
            }
        }

        public void setProperty(String property, Object value) {
            Object oldValue = properties.get(property);
            properties.put(property, value);

            if ((oldValue == null && value != null) || (oldValue != null && !oldValue.equals(value))) {
                objectsValid.clear();
                valueChangeListenerManager.fireChange();
            }
        }

        public Object getProperty(String property) {
            return properties.get(property);
        }

        public ObjectType getSource() {
            return source;
        }

        private boolean isValid() {
            for (Column column : columns) {
                if (!column.isValidValue(properties.get(column.getProperty().getPropertyName()))) {
                    return false;
                }
            }

            return true;
        }

        public ObjectType getData() {
            for (Column column : columns) {
                column.getProperty().setObjectValue(source, column.getInputCell().parseValue(
                        properties.get(column.getProperty().getPropertyName())));
            }

            return source;
        }
    }

    public void addValueChangeListener(Listener<ObjectsTable<ObjectType>> listener) {
        valueChangeListenerManager.addListener(listener);
    }
}
